Introduction
Welcome!
Code Examples
Day 1
Challenge: KISS
Solution: KISS
Day 2
Challenge: Type Annotations
Solution: Type Annotations
Day 3
Challenge: Decoupling
Solution: Decoupling
Day 4
Challenge: DRY
Solution: DRY
Day 5
Challenge: String Formatting
Solution: String Formatting
Day 6
Challenge: Law Of Demeter
Solution: Law Of Demeter
Day 7
Challenge: Better Discounts
Solution: Better Discounts
Day 8
Challenge: Payment Strategy
Solution: Payment Strategy
Day 9
Challenge: Plugins
Solution: Plugins
Day 10
Challenge: Object Oriented To Functional
Solution: Object Oriented To Functional
Day 11
Challenge: Cohesion
Solution: Cohesion
Day 12
Challenge: MVP
Solution: MVP
Day 13
Challenge: Inheritance
Solution: Inheritance
Day 14
Challenge: Abstraction
Solution: Abstraction
Day 15
Challenge: Higher-Order Functions
Solution: Higher-Order Functions
Day 16
Challenge: Configuration
Solution: Configuration
Day 17
Challenge: Concurrency
Solution: Concurrency
Day 18
Challenge: Refactoring
Solution: Refactoring
Day 19
Challenge: Itertools
Solution: Itertools
Day 20
Challenge: Inappropriate Intimacy
Solution: Inappropriate Intimacy
Wrap Up
End of Part 1

Hi team,
My solution:
************ Start Code ************
from typing import Any
from abc import ABC, abstractmethod
import requests
from dataclasses import dataclass, field
class CityNotFoundError(Exception):
pass
class InsuficientParams(Exception):
pass
class ApiGetter(ABC):
@abstractmethod
def fetch(self, url: str) -> dict[str, Any]:
"""Method that implements request library"""
@dataclass
class RequestApiGetter(ApiGetter):
timeout_value: int = 5
@property
def timeout(self) -> int:
return self.timeout_value
@timeout.setter
def timeout(self, value: int) -> None:
if isinstance(value, int):
self.timeout_value = value
else:
self.timeout_value = int(value)
def fetch(self, url: str) -> dict[str, Any]:
print(url)
return requests.get(url, timeout=self.timeout_value).json()
class ApiWeatherService(ABC):
"Implements a way to get the data "
@abstractmethod
def retrieve_forecast(self, city: str) -> None:
"""Retrieve data from a wheater API service"""
@dataclass
class WeatherService(ApiWeatherService):
api_key: str
api_getter: ApiGetter
full_weather_forecast: dict[str, Any] = field(init=False)
url = f"http://api.openweathermap.org/data/2.5/weather"
def __post_init__(self) -> None:
self.url += f"?appid={self.api_key}"
def retrieve_forecast(self, city:str) -> None:
if not city:
raise InsuficientParams("You should submit at least city and keys as params")
response = self.api_getter.fetch(f"{self.url}&q={city}")
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
self.full_weather_forecast = response
@dataclass
class MyWeatherService:
weather_service: WeatherService
def retrieve_forecast(self, city: str) -> None:
self.weather_service.retrieve_forecast(city)
# print the temperature in Celsius
temp = self.weather_service.full_weather_forecast["main"]["temp"] - 273.15
print(f"The current temperature in {city} is {temp:.1f} °C.")
if __name__ == "__main__":
api_key="1234567"
city="Mar del Plata"
api_getter = RequestApiGetter()
service = WeatherService(api_key, api_getter)
MyWeatherService(service).retrieve_forecast(city)
************ End Code ************
I could have separate the print statement to separate responsibilities but just tried to focus on remove inheritance.
Hi Alberto! Thanks for your submission!
This solution looks good! It does not rely on inheritance and uses composition. However, some minor remarks can be improved.
First and foremost, the
api_keylooks like an active key. Be sure to remove it!!Let's continue with the minor remarks:
* Usually, you would not use the
if __name__ == "__main__":statement to execute logic. Instead, define amainfunction that is called with theif __name__ == "__main__":block* Nice that you are using custom exceptions!
* Currently, this solution relies on side effects, the
self.weather_service.retrieve_forecast(city)needs to set a state fortemp = self.weather_service.full_weather_forecast["main"]["temp"] - 273.15to work properly. I would recommend thatself.weather_service.retrieve_forecast(city)instead returns a value which is later used in theretrieve_forecastmethodOther than that, this solution looks good!
Hi Andreas, how are you?
I didn't realice about the api_key :D
Thanks for the remarks!
Hi! I am doing well, and I hope you are as well!
No worries, as long as you revoked it from the service, it should be fine
Hi Arjan,
since you motivated me to think about functional approaches more often i created the following.
Idea, the printing could also be used by a home_automation calling sensors by names and returning the data.
(To be honest, the variable and function naming could be better ;-))
from functools import partial
import os
from typing import Callable
from dotenv import load_dotenv
import requests
from pydantic import BaseModel
class CityNotFoundError(Exception):
pass
class BasicWeatherInfo(BaseModel):
temp: float
feels_like: float
pressure: int
humidity: int
def weather_service(city: str, api_key: str) -> BasicWeatherInfo:
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}"
response = requests.get(url, timeout=5).json()
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return BasicWeatherInfo(**response["main"])
def create_personal_weather_service() -> Callable[[str], BasicWeatherInfo]:
load_dotenv()
api_key = os.getenv("API_KEY")
if api_key is None:
raise ValueError("API_KEY is not set")
return partial(weather_service, api_key=api_key)
def print_weather(city: str, weather_service: Callable[[str], BasicWeatherInfo]) -> None:
weather = weather_service(city)
print(f"The current temperature in {city} is {weather.temp - 273.15:.1f} °C.")
print(f"It feels like {weather.feels_like - 273.15:.1f} °C.")
print(f"The pressure is {weather.pressure} hPa.")
print(f"The humidity is {weather.humidity}%.")
if __name__ == "__main__":
personal_weather_service = create_personal_weather_service()
for city in ["Utrecht, NL", "Amsterdam, NL", "Rotterdam, NL"]:
print_weather(city, personal_weather_service)
hahaha, well naming is one (if not the) hardest thing to do in software, hopefully, it gets better with experience :( Nice to hear that you aim to use functions as a start, in my experience, it tends to lead to more understandable and testable code!
Just one remark, it is not conventional to write logic underneath
if __name__ == "__main__":. Conventionally, we want amainfunction that contains all the logic, then that we call the main function underif __name__ == "__main__":hi Arjan,
-- apologies if that question was already addressed in the chat below --
it feels a bit awkward for the client to have a temperature property, doesn't it?
wouldn't it make sense to keep the client as a client, whose job is simply to make it easy to query the API and instead capture the output of retrieve_forecast in a different data structure (let's call it WeatherForecast for the sake of the argument :)). WeatherForecast can then own the temperature property you introduced in your solution, and whatever over response parsing logic we may want to add later on...
Hi! It is more of a domain question. What you are proposing is a good solution! The challenge here was mostly to remove the abusive inheritance, but separating the client and data storage is a very good approach.
I will add that as an feedback to the course and hopefully in the future that is something that we can update!
I added two more properties. I also added a prompt for the user to enter a city. I am in the United States, so I added New York, Washington DC, Los Angeles, etc.....
@property
def humidity(self) -> float:
try:
return self.full_weather_forecast["main"]["humidity"]
except KeyError:
raise ValueError("Humidity data is not available in the forecast.")
@property
def wind_speed(self) -> float:
try:
return self.full_weather_forecast["wind"]["speed"]
except KeyError:
raise ValueError("Wind speed data is not available in the forecast.")
Then I called them in main()
# Prompt user for city input
city = input("Enter the name of the city for the weather report: ")
client = WeatherService(api_key=API_KEY)
try:
client.retrieve_forecast(city)
print(f"The current temperature in {city} is {client.temperature:.1f} °C.")
print(f"Humidity in {city} is {client.humidity}%.")
print(f"Wind speed in {city} is {client.wind_speed} meters/sec.")
except CityNotFoundError as e:
print(e)
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
except Exception as e:
print(f"An error occurred: {e}")
Nice solution!
--What I did--
from dataclasses import dataclass
from typing import Any, Protocol
import requests
KELVIN_TO_CELSIUS_CONV = 273.15
OWM_API_KEY = "blah blah blah"
OWM_BASE_URL = "http://api.openweathermap.org/data/2.5/weather"
class CityNotFoundError(Exception):
pass
def convert_kelvin_to_celsius(temp: float) -> float:
""" Convert temperature from Kelvin to Celsius """
return temp - KELVIN_TO_CELSIUS_CONV
class WeatherForecastData(Protocol):
""" Only temps for now but could have other data points (humidity, wind direction) """
@property
def temp(self) -> float:
...
class WeatherService(Protocol):
def retrieve_forecast(self, city: str) -> WeatherForecastData:
""" Return city's forecast data """
...
def retrieve_forecast(city: str, ws: WeatherService) -> WeatherForecastData:
""" Retrieve forecast data for city with any defined weather api service """
return ws.retrieve_forecast(city)
@dataclass
class OpenWeatherMapForecastData(WeatherForecastData):
base_data: dict[str, Any]
@property
def temp(self) -> float:
if "temp" not in self.base_data["main"].keys():
raise KeyError(f"Invalid key 'temp' to retrieve temperature...check api reference")
return convert_kelvin_to_celsius(self.base_data["main"]["temp"])
@dataclass
class OpenWeatherMapApi(WeatherService):
base_url: str
api_key: str
def retrieve_forecast(self, city: str) -> OpenWeatherMapForecastData:
url = f"{self.base_url}?q={city}&appid={self.api_key}"
response = requests.get(url, timeout=5).json()
if "main" not in response:
raise CityNotFoundError(
f"Couldn't find weather data. Check '{city}' if it exists and is correctly spelled.\n"
)
return OpenWeatherMapForecastData(response)
if __name__ == "__main__":
ow_service = OpenWeatherMapApi(
base_url=OWM_BASE_URL,
api_key=OWM_API_KEY,
)
city = "Utrecht"
wfd = retrieve_forecast(city, ow_service)
print(f"The current temperature in {city} is {wfd.temp:.1f} °C.")
Hmm...could've made the api call non-blocking...
Yeah, I'll go back and put at least the api key in a config file. In the videos, I hear .env files mentioned often as a way to keep things out of source control. Is .env preferable to .toml? If so, what are the advantages?
Lol, if the use of Protocols counts as inheritance, I'll have to do this one again. I'll go ahead and roll another version w/o using the inheritance syntax...I need the practice!
Hi Stanley, I personally have a preference for .env files, because they are widely supported, especially when you deal with CI/CD pipelines and containers hosted in the cloud. But I must also admit that I haven't looked at using TOML files for variables in a while, so perhaps support for them has improved.